package net.cyndeline.rldungeon.dgs.strategy.hillclimbing

import net.cyndeline.rldungeon.dgs.{Rejected, Accepted, Parameter}
import scalax.collection.GraphEdge.UnDiEdge
import net.cyndeline.rldungeon.grammar.production.{GraphProduction, Production}
import net.cyndeline.rldungeon.grammar.Strategy
import scalax.collection.immutable.Graph
import net.cyndeline.rldungeon.util.{RandomCollection, ProbabilityCollection}
import net.cyndeline.rldungeon.dgs.graph.{DGSFunctionality, Room}
import scalax.collection.GraphPredef.OuterEdge
import com.escalatesoft.subcut.inject.{BindingModule, Injectable}
import scala.reflect.runtime.universe._
import scala.reflect._
import net.cyndeline.rldungeon.dgs.strategy.ParameterResponderValidation
import net.cyndeline.rldungeon.dgs.strategy.hillclimbing.modules.{ValidatorI, ProductionIterator, ProductionIteratorI}

/**
 * Applies productions randomly in a hill-climbing fashion according to the following algorithm:
 *
 *  1. Start with a set of productions to chose from, none of which has been tried yet
 *  1. Select a production randomly and apply it, remove it from the production set
 *   - If the production didn't yield a result, select another until one is found that does.
 *   - If no yielding production was found, no more modifications can be performed: Do Exit step.
 *  1. Compare the resulting graph from the production with the previous graph using every parameters
 *  estimator object. If the number of parameters accepting the modification is higher than the ones rejecting it
 *  (by a specified percentage), the change is kept.
 *   - If any parameter with a priority rejects the change, the change is not kept and another production is selected.
 *  1. If the production was successfully applied and accepted, the set of productions are reset to its initial state,
 *  and the process starts over.
 *  1. When no more modifications are made (due to the parameter set either not agreeing on which change to keep, or
 *  because every parameter is content with the current result), run a final check to ensure that enough
 *  parameters accept the final graph (by the above mentioned percentage) and that no priority parameters reject it.
 *   - If the graph is not accepted, start over.
 *
 * @param parameters Every parameter that should be used when estimating production validity. If a change to the map
 *                   is accepted or rejected based on single-parameter priority, a parameter will have higher priority
 *                   the closer to the beginning of the list that it is.
 * @param productions All productions to be used when producing the graph, along with the probability weight
 *                     determining how likely that production is to be applied. Example: 3 productions A, B, C having
 *                     weights 1.0, 1.0 and 2.0 makes A and B 25% likely to be chosen each, and C 50% likely.
 * @param paramValidation Validator responsible for deciding if a change to the map should be kept or discarded.
 * @param attempts If the final graph is not accepted, the algorithm starts over. This number controls how many times
 *                 that is allowed to happen before the hill climber gives up.
 */
class HillClimbing[VType <: Room[VType], EType[X] <: UnDiEdge[X] with DGSFunctionality[X, EType] with OuterEdge[X, EType]]
  (parameters: List[Parameter[VType, EType]],
  productions: RandomCollection[GraphProduction[VType, EType]],
  paramValidation: ParameterResponderValidation[VType, EType],
  attempts: Int)
  (implicit val bindingModule: BindingModule)
  extends Strategy[VType, EType]
  with Injectable {

  private val productionIterator = injectOptional[ProductionIteratorI] getOrElse {
    new ProductionIterator()
  }

  /**
   * Produces a graph representation of a map.
   *
   * @param initialGraph The graph that the productions initially will be applied to.
   * @return the finished map graph if one was possible to produce according to the strategy, otherwise None.
   */
  override def apply(initialGraph: Graph[VType, EType]): Option[Graph[VType, EType]] = {
    var attemptsLeft = attempts

    while (attemptsLeft > 0) {
      val finalGraph = productionIterator.applyProductions(initialGraph, parameters, productions, paramValidation)

      /* No more modifications ended up being approved by the parameters. Run a final check to see if the graph is
       * valid (exit) or not (try again). Note that this check only runs the graphs estimated value against
       * the parameter target value range, no pre/post modification estimates are involved.
       */
      val acceptingParameters = for {
        p <- parameters
        if p.validate(finalGraph)
      } yield p

      val rejectingParameters = parameters diff acceptingParameters

      /* No need to check for rejecting parameters with priority here, a graph rejected by them would never have
       * any such updates performed on it.
       */
      if (paramValidation.levelModificationValidates(acceptingParameters.toSet, rejectingParameters.toSet))
        return Option(finalGraph)
      else
        attemptsLeft -= 1
    }

    None
  }
}
